Hotel booking

Karolina Seweryn

1. Wczytanie i przygotowanie zmiennych

Celem pracy domowej jest wyjaśnienie predykcji modelu na poziomie instacji. Modelowanie dotyczy predykcji cen doby hotelowej na podstawie zbioru Hotel Booking Demand.

In [1]:
import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt

from sklearn.preprocessing import LabelEncoder
from sklearn.model_selection import train_test_split
from sklearn.metrics import r2_score, mean_squared_error, mean_absolute_error
from sklearn.ensemble import RandomForestRegressor
from sklearn.feature_selection import RFE
from lime.lime_tabular import LimeTabularExplainer

from xgboost import XGBRegressor
In [2]:
df = pd.read_csv("./data/hotel_bookings.csv")
In [3]:
def get_integer_mapping(le):
    '''
    Return a dict mapping labels to their integer values.
    
    le: a fitted sklearn LabelEncoder
    '''
    res = {}
    for cl in le.classes_:
        res.update({cl:le.transform([cl])[0]})

    return res
In [4]:
df = df.drop(["arrival_date_year", "country", "agent", "company", "reservation_status_date", "reservation_status", "is_canceled"], axis=1)

feature_type = df.dtypes
object_features = [i for i in feature_type.index if feature_type[i] == 'object']

for feat in object_features:
    le = LabelEncoder()
    df[feat] = le.fit_transform(df[feat]) 
    integerMapping = get_integer_mapping(le)
    print(feat)
    print(integerMapping)
    
X = df.drop('adr', axis=1)
X = X.fillna(value=0)
y = df['adr']

X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=123)
hotel
{'City Hotel': 0, 'Resort Hotel': 1}
arrival_date_month
{'April': 0, 'August': 1, 'December': 2, 'February': 3, 'January': 4, 'July': 5, 'June': 6, 'March': 7, 'May': 8, 'November': 9, 'October': 10, 'September': 11}
meal
{'BB': 0, 'FB': 1, 'HB': 2, 'SC': 3, 'Undefined': 4}
market_segment
{'Aviation': 0, 'Complementary': 1, 'Corporate': 2, 'Direct': 3, 'Groups': 4, 'Offline TA/TO': 5, 'Online TA': 6, 'Undefined': 7}
distribution_channel
{'Corporate': 0, 'Direct': 1, 'GDS': 2, 'TA/TO': 3, 'Undefined': 4}
reserved_room_type
{'A': 0, 'B': 1, 'C': 2, 'D': 3, 'E': 4, 'F': 5, 'G': 6, 'H': 7, 'L': 8, 'P': 9}
assigned_room_type
{'A': 0, 'B': 1, 'C': 2, 'D': 3, 'E': 4, 'F': 5, 'G': 6, 'H': 7, 'I': 8, 'K': 9, 'L': 10, 'P': 11}
deposit_type
{'No Deposit': 0, 'Non Refund': 1, 'Refundable': 2}
customer_type
{'Contract': 0, 'Group': 1, 'Transient': 2, 'Transient-Party': 3}
In [5]:
df.shape
Out[5]:
(119390, 25)

2. Random Forest

2.1 Model

Pierwszym modelem będzie model drzewiasty Random Forest z selekcją zmiennych RFE (Recursive Feature Elimination), dla której jako model na podstawie, którego dokonuje selekcji wybrałam mniejszy model niż do finalnej predykcji.

In [6]:
rf_sel = RandomForestRegressor(max_depth=8, n_estimators=100)
selector = RFE(rf_sel, 15, step=2)
selector.fit(X_train, y_train)
Out[6]:
RFE(estimator=RandomForestRegressor(bootstrap=True, ccp_alpha=0.0,
                                    criterion='mse', max_depth=8,
                                    max_features='auto', max_leaf_nodes=None,
                                    max_samples=None, min_impurity_decrease=0.0,
                                    min_impurity_split=None, min_samples_leaf=1,
                                    min_samples_split=2,
                                    min_weight_fraction_leaf=0.0,
                                    n_estimators=100, n_jobs=None,
                                    oob_score=False, random_state=None,
                                    verbose=0, warm_start=False),
    n_features_to_select=15, step=2, verbose=0)
In [7]:
cols = selector.get_support(indices=True)

X_train, X_test = X_train.iloc[:,cols], X_test.iloc[:,cols]

rf = RandomForestRegressor(max_depth=15, n_estimators=500)
rf.fit(X_train, y_train)
Out[7]:
RandomForestRegressor(bootstrap=True, ccp_alpha=0.0, criterion='mse',
                      max_depth=15, max_features='auto', max_leaf_nodes=None,
                      max_samples=None, min_impurity_decrease=0.0,
                      min_impurity_split=None, min_samples_leaf=1,
                      min_samples_split=2, min_weight_fraction_leaf=0.0,
                      n_estimators=500, n_jobs=None, oob_score=False,
                      random_state=None, verbose=0, warm_start=False)
In [8]:
y_pred_train = rf.predict(X_train)
y_pred_test = rf.predict(X_test)

print("R2 score \ntrain: {}, \ntest: {}.".format(r2_score(y_train, y_pred_train), r2_score(y_test, y_pred_test)))
print("MSE \ntrain: {}, \ntest: {}.".format(mean_squared_error(y_train, y_pred_train), mean_squared_error(y_test, y_pred_test)))
print("MAE \ntrain: {}, \ntest: {}.".format(mean_absolute_error(y_train, y_pred_train), mean_absolute_error(y_test, y_pred_test)))
R2 score 
train: 0.8780322373321972, 
test: 0.8142339142419917.
MSE 
train: 320.49243138133255, 
test: 433.26668143654763.
MAE 
train: 10.515966071388434, 
test: 12.634597428687645.

2.2 Wyjaśnienie

Do wyjaśnienia predykcji na poziomie instancji i odpowiedzi na pytania dotyczące zmiennych wpływających na wartość predykcji użyję metody LIME.

In [54]:
exp = LimeTabularExplainer(X_train.values, mode='regression',
                           feature_names=X_train.columns.tolist(),
                            class_names=['price'],
                            categorical_features=object_features,
                            verbose=True)
Przykład 1
In [60]:
exp.explain_instance(X_train.loc[71218, :], rf.predict, num_features=8).show_in_notebook(show_table=True)
Intercept 159.81898941743975
Prediction_local [109.32282015]
Right: 108.98291194922298

Powyższy wykres przedstawia wyjaśnienie predykcji dla pojedynczej obserwacji o numerze 71218. Typ hotelu (hotel) oraz numer tygodnia przyjazdu (arrival_date_week_number) zwiększają cenę za dobę hotelową, zaś wartości zmiennych deposit_type, reserved_room_type, booking_changes, meal, adults, children zmniejszają cenę. Ostatecznie predykcja dla tej obserwacji wynosi $108.98$.

Przykład 2
In [36]:
exp.explain_instance(X_train.loc[117727, :], rf.predict, num_features=8).show_in_notebook(show_table=True)
Intercept 147.8781141784561
Prediction_local [104.94704997]
Right: 85.74765720639007

Powyższy wykres przedstawia wyjaśnienie predykcji dla pojedynczej obserwacji o numerze 117727. Typ hotelu (hotel) oraz numer tygodnia przyjazdu (arrival_date_week_number) zwiększają cenę za dobę hotelową, zaś wartości zmiennych reserved_room_type, deposit_type, meal, market_segment, adults, booking_changes zmniejszają cenę. Ostatecznie, model przewidział cenę dla tej obserwacji równą $85.75$.

Przykład 3

Wyjaśnienie predykcji dla obserwacji, dla której model przewidział największą cenę

In [50]:
w = rf.predict(X_train)
ind = X_train.index.to_list()[np.argmax(w)]
In [53]:
exp.explain_instance(X_train.loc[ind, :], rf.predict, num_features=10).show_in_notebook(show_table=True)
Intercept 137.1974982483532
Prediction_local [105.76021229]
Right: 3276.4951270495503

Powyższy wykres przedstawia wyjaśnienie predykcji dla pojedynczej obserwacji, dla której model przewidział najwzyższy wynik w zbiorze treningowym (3276.50). Można zauważyć, że na podwyższenie ceny doby hotelowej mają wpływ wartości zmiennych hotel, deposit_type, booking_changes, lead_time, a obniżają cenę wartości zmiennych arrival_date_week_number, reserved_room_type, meal, adults, childer, market_segment. Klient ten dokonał 1 miany w rezerwacji, więc zgodnie z logiką ta wartość wpływa na zwiększenie ceny. Wybór opcji jedynie ze śniadaniami zmniejszył cenę. Rezerwacja została dokonana dla 2 dorosłych bez dzieci, co również zmniejszyło cenę.

3. XGBoost

3.1 Dopasowanie modelu

In [95]:
xgb = XGBRegressor()
xgb.fit(X_train.values, y_train.values)
Out[95]:
XGBRegressor(base_score=0.5, booster=None, colsample_bylevel=1,
             colsample_bynode=1, colsample_bytree=1, gamma=0, gpu_id=-1,
             importance_type='gain', interaction_constraints=None,
             learning_rate=0.300000012, max_delta_step=0, max_depth=6,
             min_child_weight=1, missing=nan, monotone_constraints=None,
             n_estimators=100, n_jobs=0, num_parallel_tree=1,
             objective='reg:squarederror', random_state=0, reg_alpha=0,
             reg_lambda=1, scale_pos_weight=1, subsample=1, tree_method=None,
             validate_parameters=False, verbosity=None)
In [96]:
y_pred_train = xgb.predict(X_train.values)
y_pred_test = xgb.predict(X_test.values)

print("R2 score \ntrain: {}, \ntest: {}.".format(r2_score(y_train, y_pred_train), r2_score(y_test, y_pred_test)))
print("MSE \ntrain: {}, \ntest: {}.".format(mean_squared_error(y_train, y_pred_train), mean_squared_error(y_test, y_pred_test)))
print("MAE \ntrain: {}, \ntest: {}.".format(mean_absolute_error(y_train, y_pred_train), mean_absolute_error(y_test, y_pred_test)))
R2 score 
train: 0.8539162911536008, 
test: 0.8209720437524022.
MSE 
train: 383.8614565793326, 
test: 417.5511809448795.
MAE 
train: 12.610872641221311, 
test: 13.278806309380023.

3.2 Wyjaśnienie

Przykład 1
In [99]:
exp.explain_instance(X_train.loc[71218, :], xgb.predict, num_features=8).show_in_notebook(show_table=True)
Intercept 161.78230691928374
Prediction_local [105.63845735]
Right: 107.15362

Powyższy wykres przedstawia wyjaśnienie predykcji dla obserwacji o indeksie 71218 i modelu XGBoost. Możemy zauważyć, że w wyjaśnieniu tej obserwacji w poprzednim modelu nie znajdowała się zmienna market_segment.

Przykład 2
In [100]:
exp.explain_instance(X_train.loc[117727, :], xgb.predict, num_features=8).show_in_notebook(show_table=True)
Intercept 139.93341377725133
Prediction_local [108.16871958]
Right: 97.16512

Dla tej obserwacji również możemy zauważyć różnicę. W modelu RF piątą zmienną była zmienna adult, a w tym przypadku jest to previous_cancellations. Co ciekawe, brak wcześniejszych anulowań rezerwacji zwiększa cenę za dobę hotelową

Przykład 3
In [104]:
exp.explain_instance(X_train.loc[ind, :], xgb.predict, num_features=10).show_in_notebook(show_table=True)
Intercept 96.7403738931637
Prediction_local [191.44236862]
Right: 4043.305

Powyżej przedstawiam przypadek największej wartości predykcji w przypadku obu modeli. Wyjaśnienie wartości tej predykcji w przypadku modelu Random Forest i XGBoost różni się nieznacznie. W wyjaśnieniu modelu XGBoost pojawiają się zmienne previous_cancellarions i arrival_date_day_of_month, które nie pojawiły się w wyjaśnieniu predykcji modelu Random Forest dla tej obserwacji